package network;

import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;

import network.config.NetworkConfiguration;
import network.config.NetworkConfiguration.PortType;

import controller.MessageController;
import controller.UserNotConnected;

/**
 * Abstract network interface that is able to discuss on
 * network : The interface receive all incoming signals
 * and call appropriate method from controller. This interface
 * is also the only way to send signal on the network.
 */
public abstract class AbstractNetworkInterface extends Thread {

	private static final ArrayList <InterfaceAddress> LOCAL_INTERFACES = 
			new ArrayList <InterfaceAddress> () ;
	
	static {
		try {
			for (NetworkInterface ni : Collections.list(NetworkInterface.getNetworkInterfaces())) {
				for (InterfaceAddress ia : ni.getInterfaceAddresses()) {
					if (ia != null) {
						LOCAL_INTERFACES.add(ia) ;
					}
				}
			}
		} 
		catch (SocketException e) {
		}
	}
	
	/**
	 * Get all the available network interfaces, even the loopback and link local interfaces.
	 * 
	 * @return the available interfaces
	 */
	public static ArrayList <InterfaceAddress> getAllLocalInterfaces () {
		return LOCAL_INTERFACES ;
	}
	
	private LinkedList <AbstractUserSignal> signals ;
	private boolean running ;
	private UDPListener listener ;
	protected UDPSender sender ;
	protected MessageController msgCtrl = null ;
	protected NetworkConfiguration networkConfig = null ;
	
	public AbstractNetworkInterface (MessageController msgCtrl, NetworkConfiguration netConf) throws ListenerBindException {
		this.networkConfig = netConf ;
		try {
			this.listener = new UDPListener(this.networkConfig, this) ;
		} 
		catch (SocketException e) {
			throw new ListenerBindException("Unable to bind listener to port : " + String.valueOf(this.networkConfig.getPort(PortType.UDP_RECEIVE))) ;
		}
		try {
			this.sender = new UDPSender(this.networkConfig) ;
		}
		catch (SocketException e) {
			e.printStackTrace() ;
		}
		this.signals = new LinkedList <AbstractUserSignal> () ;
		this.msgCtrl = msgCtrl ;
	}
	
	/**
	 * Perform action corresponding to the specified signal using
	 * the message controller.
	 * 
	 * @param asi An AbstractUserSignal that contains signal and address of sender
	 * @throws UserNotConnected if the local user is not connected
	 * @throws SendSignalException if an error occurs sending a response signal (see MessageController for more information)
	 * @throws UnknownHostException if there is any problem with InetAddress (sender and receiver)
	 */
	protected abstract void performSignal (AbstractUserSignal asi) 
			throws UnknownHostException, SendSignalException, UserNotConnected ;
	
	/**
	 * Terminate the Thread properly
	 */
	public synchronized void finish () {
		this.running = false ;
	}
	
	/**
	 * Add the specified signal to the end of the signals queue.
	 * 
	 * @param aus
	 */
	protected final void addAbstractSignal (AbstractUserSignal aus) {
		synchronized (this.signals) {
			this.signals.addLast(aus) ;
		}
	}
	
	/**
	 * Remove and return the older signal from the queue.
	 * 
	 * @return The older signal
	 */
	protected final AbstractUserSignal getFirstSignal () {
		synchronized (this.signals) {
			return this.signals.removeFirst () ;
		}
	}
	
	/**
	 * Add a signal to the queue
	 * 
	 * @param packet The packet to serialize into a new UserSignal
	 */
	protected abstract void addSignal (DatagramPacket packet) ;

	/* (non-Javadoc)
	 * @see java.lang.Thread#run()
	 */
	public final void run () {
		this.listener.start () ;
		this.running = true ;
		while (this.running) {
			if (!this.signals.isEmpty()) {
				try {
					performSignal (getFirstSignal()) ;
				} 
				catch (Exception e) {
					e.printStackTrace() ;
				}
			}
			else {
				synchronized (this) {
					try {
						wait() ;
					} 
					catch (InterruptedException e1) {
					}
				}
			}
		}
		this.listener.finish () ;
	}
	
	/**
	 * Send a "Hello" signal on broadcast with specified local username.
	 * 
	 * @param username The local username
	 * @throws SendSignalException if an error occurs sending the signal
	 */
	public abstract void sendHello (String username) throws SendSignalException ;
	
	/**
	 * Send a "HelloReply" signal to the specified user with specified local username.
	 * 
	 * @param username The local username
	 * @param remoteUser The user that must receive the HelloReply signal
	 * @throws SendSignalException if an error occurs sending the signal
	 */
	public abstract void sendHelloReply (String username, InetAddress remoteAddress) throws SendSignalException ;
	
	/**
	 * Send a "GoodBye" signal on broadcast
	 * 
	 * @throws SendSignalException if an error occurs sending the signal
	 */
	public abstract void sendGoodBye () throws SendSignalException ;
	
	/**
	 * Send a "AskLogin" signal to the specified address
	 * 
	 * @param to The user that will receive the "AskLogin" signal and respond with a "HelloReply" signal
	 * @throws SendSignalException if an error occurs sending the signal
	 */
	public abstract void sendAskLogin (InetAddress to) throws SendSignalException ;
	
	/**
	 * Send a "SendText" signal to the specified user
	 * 
	 * @param message The message to send
	 * @param to The remote user, or null for broadcast
	 * @param cc All the user that will receive the message (including, if necessary, the "to" user)
	 * @throws SendSignalException if an error occurs sending the signal
	 */
	public abstract void sendMessage (String message, InetAddress to, InetAddress[] cc) throws SendSignalException ;
	
	/**
	 * Send a "PropFile" signal to the specified user
	 * 
	 * @param filename The name of the file
	 * @param fileSize The size of the file
	 * @param fileID The unique ID of the file (ID must be unique for each senders)
	 * @param to The remote user that will receive the proposition
	 * @throws SendSignalException if an error occurs sending the signal
	 */
	public abstract void sendPropFile (String filename, long fileSize, int fileID, InetAddress to) throws SendSignalException ;
	
	/**
	 * Send a "AcceptFile" signal to the receiver
	 * 
	 * @param to The receiver that want to send the file and will receive "AcceptFile" signal
	 * @param fileID The ID of the file, send by the receiver (see sendPropFile for information about uniqueness)
	 * @param accepted true if the user want to receive the file
	 * @param now true if the user want to receive the file now, if accepted is false, this parameter has no meaning
	 * @throws SendSignalException if an error occurs sending the signal
	 */
	public abstract void sendAcceptFile(InetAddress to, int fileID, boolean accepted, boolean now) throws SendSignalException ;	
	
}
